/*
 * RemoteMethod.java    0.0.1    Nov 3, 2009
 * 
 * Copyright (c) 2009 mentalsmash.org
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * The use of the Apache License does not indicate that this project is
 * affiliated with the Apache Software Foundation.
 */
package org.mentalsmash.tazio.net.ril;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;

import org.mentalsmash.tazio.net.ril.annotations.ArgumentsByCopy;
import org.mentalsmash.tazio.net.ril.annotations.InvokeAtMostOnce;
import org.mentalsmash.tazio.net.ril.annotations.InvokeOnGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class describes a method exposed to remote invocation. It provides
 * all necessary informations about it and it can be constructed from a
 * {@link Method} instance.
 * 
 * @version 0.0.1 Nov 3, 2009
 * @author Andrea Sorbini <as@mentalsmash.org>
 *
 */
public class RemoteMethod implements Serializable {
	private static final Logger log = LoggerFactory.getLogger(RemoteMethod.class);
	
	private static final long serialVersionUID = -6253545618012472787L;

	private final InvocationSemantics semantics;
	private final String name;
	private final Class<?>[] paramTypes;
	private final Class<?> returnType;
	private final Class<?> declaringClass;
	private final Class<?>[] exceptionTypes;
	private final boolean byCopy;

	/**
	 * Creates a new RemoteMethod taking the informations from the supplied {@link Method}
	 * @param m a Method instance from which to build the new RemoteMethod
	 */
	public RemoteMethod(Method m) {
		Annotation semanticsAnnotation = m
				.getAnnotation(InvokeAtMostOnce.class);
		if (semanticsAnnotation != null) {
			this.semantics = InvocationSemantics.AtMostOnce;
		} else {
			semanticsAnnotation = m.getAnnotation(InvokeOnGroup.class);
			if (semanticsAnnotation != null) {
				this.semantics = InvocationSemantics.OnGroup;
			} else {
				this.semantics = InvocationSemantics.Default;
			}
		}
		
		Annotation byCopy = m.getAnnotation(ArgumentsByCopy.class);
		if (byCopy != null) {
			this.byCopy = true;
		} else {
			this.byCopy = false;
		}

		this.name = m.getName();
		this.paramTypes = m.getParameterTypes();
		this.returnType = m.getReturnType();
		this.declaringClass = m.getDeclaringClass();
		this.exceptionTypes = m.getExceptionTypes();
	}
	
	/**
	 * This method substitutes any instance of a "primitive type class" found
	 * in the supplied array with the corresponding "primitive type wrapping class"
	 * (e.g. Character.TYPE is replaced by Character.class).
	 * 
	 * @param paramTypes an array of {@link Class} instances to be checked.
	 * @return a new array containing the filtered original elements.
	 */
	private Class<?>[] getWithoutPrimitiveParamTypes(Class<?>[] paramTypes){
		ArrayList<Class<?>> params = new ArrayList<Class<?>>();
		
		for (int i = 0; i < paramTypes.length; i++) {
			Class<?> param = paramTypes[i];
			
			if (param.isPrimitive()) {
				if (param.equals(Character.TYPE)){
					params.add(Character.class);
				} else if (param.equals(Byte.TYPE)){
					params.add(Byte.class);
				} else if (param.equals(Boolean.TYPE)){
					params.add(Boolean.class);
				} else if (param.equals(Integer.TYPE)){
					params.add(Integer.class);
				} else if (param.equals(Short.TYPE)){
					params.add(Short.class);
				} else if (param.equals(Double.TYPE)){
					params.add(Double.class);
				} else if (param.equals(Float.TYPE)){
					params.add(Float.class);
				} else if (param.equals(Long.TYPE)){
					params.add(Long.class);
				}
			} else {
				params.add(param);
			}
			
		}
		
		return params.toArray(new Class<?>[0]);
	}

	/**
	 * This enum is used to identify a method's invocation semantics
	 * 
	 * @author Andrea Sorbini <as@mentalsmash.org>
	 * 
	 */
	public enum InvocationSemantics {
		Default, AtMostOnce, OnGroup
	}

	/**
	 * @return the invocation semantics of this method 
	 */
	public InvocationSemantics getSemantics() {
		return semantics;
	}

	/**
	 * @return the name of this method
	 */
	public String getName() {
		return name;
	}

	/**
	 * @return an array of {@link Class} containing the parameter types of this method
	 */
	public Class<?>[] getParamTypes() {
		return paramTypes;
	}

	/**
	 * @return the return type of this method
	 */
	public Class<?> getReturnType() {
		return returnType;
	}

	/**
	 * @return the class that declares this method (usually an interface)
	 */
	public Class<?> getDeclaringClass() {
		return declaringClass;
	}

	/**
	 * @return an array containing the types of the exceptions thrown by this method
	 */
	public Class<?>[] getExceptionTypes() {
		return exceptionTypes;
	}
	
	/**
	 * 
	 * @return true if arguments supplied by this method are passed by copy (i.e. they won't support
	 * "back-calling" - @see RemoteInvocationDispatcher for further details about passing arguments
	 * to remote invocations)
	 */
	public boolean isByCopy(){
		return this.byCopy;
	}


	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		/*
		 * result = prime * result + ((declaringClass == null) ? 0 :
		 * declaringClass.hashCode()); result = prime * result +
		 * Arrays.hashCode(exceptionTypes);
		 */
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		result = prime * result + Arrays.hashCode(paramTypes);
		/*
		 * result = prime * result + ((returnType == null) ? 0 :
		 * returnType.hashCode()); result = prime * result + ((semantics ==
		 * null) ? 0 : semantics.hashCode());
		 */
		return result;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		
		if (obj.getClass().equals(Method.class)){
			return this.equalsMethod((Method)obj);
		}
		
		if (getClass() != obj.getClass())
			return false;
		RemoteMethod other = (RemoteMethod) obj;

		if (other.name.equals(this.name)) {
			
			if (this.paramTypes == null) {
				return other.paramTypes == null;
			}
			
			for (Class<?> paramType : other.paramTypes) {
				boolean hasIt = false;

				for (Class<?> pt : this.paramTypes) {
					if (pt.equals(paramType)) {
						hasIt = true;
						break;
					}
				}

				if (!hasIt)
					return false;
			}
			
			return true;
		}

		return false;
	}

	/**
	 * This method checks if the supplied method is the same as
	 * the one identified by this RemoteMethod.
	 * 
	 * @param m the method to check
	 * @return true if  the supplied method is the same as the one identified by this RemoteMethod
	 */
	private boolean equalsMethod(Method m) {
		return (this.name.equals(m.getName()) && ((this.paramTypes == null && m
				.getParameterTypes() == null) || (Arrays.equals(
				this.paramTypes, m.getParameterTypes()))));
	}

	public String toString() {
		StringBuilder value = new StringBuilder();
		value.append("[ method = ");

//		value.append(this.returnType.getCanonicalName() + " ");
		value.append(this.declaringClass.getSimpleName()+"#"+this.name + "(");

		for (int i = 0; i < this.paramTypes.length; i++) {
			Class<?> param = this.paramTypes[i];

			value.append(param.getSimpleName());

			if (i != (this.paramTypes.length - 1)) {
				value.append(", ");
			}
		}

		
		
		value.append(")");

		if (this.exceptionTypes.length > 0){
			value.append(" throws ");
			
			for (int i = 0; i < this.exceptionTypes.length; i++) {
				Class<?> exc = this.exceptionTypes[i];

				value.append(exc.getSimpleName());

				if (i != (this.exceptionTypes.length - 1)) {
					value.append(", ");
				}
			}
		}
		
		
		value.append("; ");
		
		value.append("returnType = "+this.returnType.getSimpleName()+"; ");
		
		value.append("semantics = ");
		
		switch (this.semantics) {
		case Default:
			value.append("default");
			break;
		case AtMostOnce:
			value.append("atMostOnce");
			break;
		case OnGroup:
			value.append("onGroup");
			break;
		}

		value.append(" ]");

		return value.toString();
	}

	/**
	 * Checks whether the supplied array of arguments would
	 * be compatible with an invocation of this method. 
	 * 
	 * @param args an array of invocation arguments to check
	 * @return true if the supplied arguments might be used to invoke this method
	 */
	public boolean checkArgs(Object[] args) {
		if ((args == null || args.length == 0)&& this.paramTypes.length == 0)
			return true;
		
		if (args == null || args.length != this.paramTypes.length)
			return false;
		
		Class<?>[] paramTypes = this.getWithoutPrimitiveParamTypes(this.paramTypes);
		
		for (int i = 0; i < args.length; i++) {
			Object arg = args[i];
			Class<?> paramClass = paramTypes[i];
			
			if (!paramClass.isInstance(arg)) {
				return false;
			}
		}
		
		return true;
	}
	
	
	/**
	 * @return a {@link Method} instance for the method identified by this RemoteMethod.
	 * @throws NoSuchMethodException if the identified method wasn't found in the local JVM
	 * @throws ClassNotFoundException if the declaring class of this method wasn't found in the local JVM 
	 */
	public Method getMethod() throws ClassNotFoundException, NoSuchMethodException {
		try {
			Class<?> decClass = Class.forName(this.declaringClass.getCanonicalName());
			return decClass.getMethod(this.name, this.paramTypes);
		} catch (ClassNotFoundException e) {
			log.warn("Class "+this.declaringClass+" was not found in the local JVM, this is weird");
			throw e;
		} catch (NoSuchMethodException e){
			log.warn("Method "+this.name+" not found on class "+this.declaringClass+", this is weird");
			throw e;
		}
	}
}
